﻿using System;
using System.Collections;
using System.Collections.Generic;
using TFramework.ECS.Blueprint;
using TFramework.ECS.Component.Manager;
using TFramework.ECS.Entity;
using TFramework.ECS.Entity.Manager;
using TFramework.ECS.Event;
using TFramework.ECS.Group;
using TFramework.ECS.Group.Manager;
using TFramework.ECS.Group.Observable;
using TFramework.Extensions;
using UniRx;

namespace TFramework.ECS.Collection
{
    public class EntityCollection : IEntityCollection, INotifyingEntityCollection
    {
        public IEntityFactory EntityFactory { get; }

        public readonly EntityLookup EntityLookup;
        public readonly IDictionary<int, IDisposable> EntitySubscriptions;

        public INotifyingEntityCollection notifyingEntityCollection;
        public IObservable<CollectionEntityEvent> EntityAdded => _onEntityAdded;
        public IObservable<CollectionEntityEvent> EntityRemoved => _onEntityRemoved;
        public IObservable<ComponentsChangedEvent> EntityComponentsAdded => _onEntityComponentsAdded;
        public IObservable<ComponentsChangedEvent> EntityComponentsRemoving => _onEntityComponentsRemoving;
        public IObservable<ComponentsChangedEvent> EntityComponentsRemoved => _onEntityComponentsRemoved;
        public IObservableGroupFactory ObservableGroupFactory { get; }
        public IComponentTypeLookup ComponentTypeLookup { get; }

        public int Count => throw new NotImplementedException();

        public IEntity this[int index] => throw new NotImplementedException();

        private readonly ObservableGroupLookup _observableGroups;

        private readonly Subject<CollectionEntityEvent> _onEntityAdded;
        private readonly Subject<CollectionEntityEvent> _onEntityRemoved;
        private readonly Subject<ComponentsChangedEvent> _onEntityComponentsAdded;
        private readonly Subject<ComponentsChangedEvent> _onEntityComponentsRemoving;
        private readonly Subject<ComponentsChangedEvent> _onEntityComponentsRemoved;


        public EntityCollection(IEntityFactory entityFactory, IObservableGroupFactory observableGroupFactory, IComponentTypeLookup componentTypeLookup)
        {
            ObservableGroupFactory = observableGroupFactory;
            ComponentTypeLookup = componentTypeLookup;

            EntityLookup = new EntityLookup();
            EntitySubscriptions = new Dictionary<int, IDisposable>();
            EntityFactory = entityFactory;

            _observableGroups = new ObservableGroupLookup();
            _onEntityAdded = new Subject<CollectionEntityEvent>();
            _onEntityRemoved = new Subject<CollectionEntityEvent>();
            _onEntityComponentsAdded = new Subject<ComponentsChangedEvent>();
            _onEntityComponentsRemoving = new Subject<ComponentsChangedEvent>();
            _onEntityComponentsRemoved = new Subject<ComponentsChangedEvent>();
        }

        public void SubscribeToEntity(IEntity entity)
        {
            var entityDisposable = new CompositeDisposable();
            entity.ComponentsAdded.Subscribe(x => _onEntityComponentsAdded.OnNext(new ComponentsChangedEvent(entity, x))).AddTo(entityDisposable);
            entity.ComponentsRemoving.Subscribe(x => _onEntityComponentsRemoving.OnNext(new ComponentsChangedEvent(entity, x))).AddTo(entityDisposable);
            entity.ComponentsRemoved.Subscribe(x => _onEntityComponentsRemoved.OnNext(new ComponentsChangedEvent(entity, x))).AddTo(entityDisposable);
            EntitySubscriptions.Add(entity.Id, entityDisposable);
        }

        public void UnsubscribeFromEntity(int entityId)
        { EntitySubscriptions.RemoveAndDispose(entityId); }

        public IEntity CreateEntity(IBlueprint blueprint = null, int? id = null)
        {
            if (id.HasValue && EntityLookup.Contains(id.Value))
            { throw new InvalidOperationException("id already exists"); }

            var entity = EntityFactory.Create(id);

            EntityLookup.Add(entity);
            _onEntityAdded.OnNext(new CollectionEntityEvent(entity));
            SubscribeToEntity(entity);

            blueprint?.Apply(entity);

            return entity;
        }

        public IEntity GetEntity(int id)
        { return EntityLookup[id]; }

        public void RemoveEntity(int id, bool disposeOnRemoval = true)
        {
            var entity = GetEntity(id);
            EntityLookup.Remove(id);

            var entityId = entity.Id;

            if (disposeOnRemoval)
            {
                entity.Dispose();
                EntityFactory.Destroy(entityId);
            }

            UnsubscribeFromEntity(entityId);

            _onEntityRemoved.OnNext(new CollectionEntityEvent(entity));
        }

        public void AddEntity(IEntity entity)
        {
            EntityLookup.Add(entity);
            _onEntityAdded.OnNext(new CollectionEntityEvent(entity));
            SubscribeToEntity(entity);
        }

        public bool ContainsEntity(int id)
        { return EntityLookup.Contains(id); }

        public void Dispose()
        {
            _onEntityAdded.Dispose();
            _onEntityRemoved.Dispose();
            _onEntityComponentsAdded.Dispose();
            _onEntityComponentsRemoving.Dispose();
            _onEntityComponentsRemoved.Dispose();

            EntityLookup.Clear();
            EntitySubscriptions.RemoveAndDisposeAll();
        }

        public IEnumerable<IEntity> GetEntitiesFor(IGroup group)
        {
            if (group is EmptyGroup)
            { return new IEntity[0]; }
            return EntityLookup.MatchingGroup(group);
        }

        public IEnumerable<IEntity> GetEntitiesFor(GroupLookup groupLookup)
        {
            if (groupLookup.RequiredComponents.Length == 0 && groupLookup.ExcludedComponents.Length == 0)
            { return new IEntity[0]; }

            return EntityLookup.MatchingGroup(groupLookup);
        }

        public IObservableGroup GetObservableGroup(IGroup group)
        {
            var requiredComponents = ComponentTypeLookup.GetComponentTypes(group.RequiredComponents);
            var excludedComponents = ComponentTypeLookup.GetComponentTypes(group.ExcludedComponents);
            var lookupGroup = new GroupLookup(requiredComponents, excludedComponents);

            var observableGroupToken = new ObservableGroupToken(lookupGroup);
            if (_observableGroups.Contains(observableGroupToken))
            { return _observableGroups[observableGroupToken]; }

            var entityMatches = GetEntitiesFor(lookupGroup);
            var configuration = new ObservableGroupConfiguration
            {
                ObservableGroupToken = observableGroupToken,
                InitialEntities = entityMatches,
                NotifyingCollections = this
            };

            var observableGroup = ObservableGroupFactory.Create(configuration);
            _observableGroups.Add(observableGroup);

            return _observableGroups[observableGroupToken];
        }

        public IEnumerator<IEntity> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }
}